2
上一篇文章:Python--Redis实战:第一章:初识Redis:第三节:你好Redis-文章投票试炼
下一篇文章:Python--Redis实战:第二章:使用Redis构建Web应用:第二节:使用Redis实现购物车

从高层次的角度来看,Web应用就是通过HTTP协议对网页浏览器发送的请求进行响应的服务器或者服务【service】。一个Web服务器对请求进行响应的典型步骤如下:

  • 服务器对客户端发来的请求【request】进行解析。
  • 请求被转发给一个预定义的处理器【handler】
  • 处理器可能会从数据库取出数据
  • 处理器根据取出的数据对模板【template】进行渲染(render)
  • 处理器向客户端返回渲染后的内容作为对请求的响应【response】

以上列举的5个步骤从高层次的角度展示了典型Web服务器的运作方式,这种情况下的Web请求被认为是无状态的【stateless】,也就是说,服务器本身不会记录与过往有关的任何信息,这使得失效【fail】的服务器可以很容易地被替换掉。有不少书籍专门介绍了如何优化响应过程的各个步骤,本章要做的事情也类似,不同之处是,我们将介绍如何使用更快的Redis查询来替代传统的关系数据库查询,已经如何使用Redis来完场一些使用关系数据库没有办法高效完场的任务。

本章的所有内容都是围绕着发现并解决【Fake Web Retailer】这个虚构的大型网上商店来展开的,这个商店每天都会有大约500万名不同的用户,这些用户会给网站带来一亿次点击,并从网站购买超过10万件商品。我们之所以将这几个数据量设置的特别大,是考虑【如果可以在大数据背景下顺利解决问题,那么解决小数据量和中等数据量引发的问题就更不在话下】。

登录和cookie缓存

每当我们登录互联网服务的时候,这些服务都会使用cookie来记录我们的身份。cookie由少量数据组成,网站会要求我们的浏览器存储这些数据,并在每次服务器请求发送时将这些数据传回给服务器。对于用来登录的cookie,有两种常见的方式可以将登录信息存储在cookie里面:

  • 签名【signed】cookie
  • 令牌【token】cookie

签名cookie通常会存储用户名,可能还有用户ID,用户最后一次登录成功的时间,以及网站觉得有用的其他任何信息。除了用户的相关信息之外,签名cookie还包含了一个签名,服务器可以使用这个签名来验证浏览器发送的消息是否未经改动(比如将cookie中的登录用户名改成另一个用户)。

令牌cookie会在cookie里面存储一串随机字节作为令牌,服务器可以根据令牌在数据库中查询令牌的拥有者。随着时间的推移,旧令牌会被新令牌去掉。

cookie类型 优点 缺点
签名cookie 验证cookie所需的一切信息都存储在cookie里面,cookie可以包含额外的信息,并且对这些信息进行签名也很容易。 正确的处理签名很难,很容易忘记对数据进行签名,或者忘记验证数据的签名,从而造成安全漏洞。
令牌cookie 添加信息非常容易,cookie的体积非常小,因此移动终端和速度较慢的客户端可以更快地发送请求。 需要在服务中存储更多信息,如果使用的是关系数据库,那么载入和存储的cookie的代价可能会很高。

这次我们使用令牌cookie来引用关系数据库表中负责存储用户登录信息的条目【entry】。除了用户登录信息之外,我们还可以将用户的访问时长和已浏览商品的数量等信息存储到数据库里面,这样便于将来通过分析这些信息来学习如果更好得向用户推销商品。

一般来说,用户在决定购买某个或某些商品之前,通常都会先浏览多个不同商品,而记录用户浏览过的所有商品以及用户最后一次访问页面的时间等信息,通常会导致大量的数据库写入。从长远来看,用户的这些浏览数据的确非常有用,但问题是,即便经过优化,大多数关系数据库在每台数据库服务器上每秒也只能插入、更细或者删除200~2000个数据行。尽量批量插入、批量更新和批量删除等操作可以更快地速度执行,但因为客户端每次浏览网页都只更新少数几行,所以高速的批量插入在这里并不适用。

我们假设我们的网站每天的负载量都比较大:平均每秒大约1200次写入,高峰时期每秒接近6000次写入,所以它必须部署10台关系数据服务器才能应对高峰时期的负载量。而我们要做的就是适用Redis重新实现登录cookie功能,取代由关系数据库实现的登录cookie功能。

首先,我们将使用一个散列来存储登录cookie令牌和已登录用户之间的映射。要检查一个用户是否已经登录,需要根据给定的令牌来查找与之对应的用户,并在用户已经登录的情况下,返回该用户的ID。

def check_token(conn,token):
    #尝试获取并返回令牌对应的用户
    return conn.hget('login:',token)

对令牌进行检查并不困难,因为大部分复杂的工作都是在更新令牌时完成的:用户每次浏览页面时,程序都会对用户存储在登录散列里面的信息进行更新,并将用户的令牌和当前时间戳添加到记录最近登录用户的有序集合里面;如果用户正在浏览的是一个商品页面,那么程序还会将这个商品添加到记录这个用户最近浏览过的商品的有序集合里面,并在被记录商品的数据超过25个时,对这个有序集合进行修建。

#更新令牌
import time
def update_token(conn,token,user,item=None):
    timestamp=time.time() #h获取当前时间戳
    conn.hset('login:',token,user) #维持令牌与已登陆用户之间的映射
    conn.zadd('recent:',token,timestamp) #记录领哦哎最后一次出现的时间
    if item:
        conn.zadd('viewed:'+token,item,timestamp) #记录用户浏览郭的商品
        conn.zremrangebyrank('viewed:'+token,0,-26) #移除旧的记录,值保留用户最近浏览过的25个商品

通过update_token()函数,我们可以记录用户最后一次浏览商品的时间以及用户最近浏览了哪些商品。在一台最近几年生产的服务器上面,使用update_token()函数每秒至少记录20000件商品,这比我们预估的网站高峰期所需的6000次写入要高3倍有余。不仅如此,通过后面介绍的一些方法,我们还可以进一步优化update_token()函数的运行速度。但在优化前,性能也比原有的关系数据库性能提升了10~100倍。

因为存储会话数据所需的内存会随着时间的推移而不断增加,所以我们需要定期清理旧的会话数据,为了限制会话数据的数量,我们决定只保留最新的1000万个会话。清理旧会话的程序由一个循环构成,这个循环每次执行的时候,都会检查存储最新登录令牌的有序集合大小,如果有序集合的大小超过了限制,那么程序就会从有序集合里面移除最多100个最旧的令牌,并从记录用户登录页面的散列表里面,移除被删除令牌对应的用户的信息,并对存储了这些用户最近浏览商品记录的有序集合进行清理。如果令牌的数量未超过限制,那么程序会休眠1秒,之后再重新进行检查。

#清理旧会话
import time

QUIT=False
LIMIT=10,000,000

def clean_sessions(conn):
    while not QUIT:
        #目前已有令牌的数量
        size=conn.zcard('recent:')
        if size<=LIMIT:
            #令牌数量未超过限制,休眠1秒后再重新检查
            time.sleep(1)
            continue
        end_index=min(size-LIMIT,100)
        tokens=conn.zrange('recent:',0,end_index-1)

        session_keys=[]
        #为那些将要删除的令牌构建键名
        for token in tokens:
            session_keys.append('viewed:'+token)
        #移除最旧的那些令牌
        conn.delete(*session_keys)
        conn.hdel('login:',*tokens)
        conn.zrem('recent:',*tokens)

让我们通过计算来了解一下,这段简短的代码为什么能够妥善地处理每天500万人次的访问:假设网站每天有500万用户访问,并且每天的用户都和之前的不一样,那么只需要两天,令牌的数量就会达到1000万上限,并将网站的内存空间销毁殆尽,因为一天有:24*3600=86400秒,而网站平均每秒产生5 000 000/86400<58个新会话,如果清理函数以每秒的频率运行,那么它每秒需要清理将近60个令牌,才能防止令牌的数量过多的问题发生。但是实际上,我们定义的令牌清理函数在通过网络来运行时,每秒能够清理10 000多个令牌,在本地运行时,每秒能够清理60 000多个令牌,这比所需的清理速度快乐150~1000倍,所以因为旧令牌过多而导致网站空间耗尽的问题不会出现。

熟悉多线程编程或者并发编程的读者可能会发现上面的清理函数包含了一个竞争条件【race condition】:如果清理函数正在删除某个用户的信息,而这个用户又在同一时间访问网站的话,那么竞争条件就会导致用户的信息被错误的删除。目前来看,这个竞争条件除了会使得用户需要重新登录一次之外,并不会对程序记录的数据产生明显的影响,所以我们暂时搁置这个问题,之后会讲解防止类似的竞争条件发生的方法。

通过使用Redis来记录用户信息,我们成功地将每天要对数据库执行的行写入操作减少了数百万次。虽然这非常的了不起,但这只是我们使用Redis构建Web应用程序的第一步,接下来我们将展示如何使用Redis来处理另一种类型的cookie。

上一篇文章:Python--Redis实战:第一章:初识Redis:第三节:你好Redis-文章投票试炼
下一篇文章:Python--Redis实战:第二章:使用Redis构建Web应用:第二节:使用Redis实现购物车

Mark
662 声望344 粉丝

talk is cheap,show me the code